Creating Mock-Based Tests Using Mockito
Course: Advance Java Training
Topic: Creating Mock-Based Tests Using Mockito
Course Link: Advance Java Training – JustAcademy
Demo Link: Register for a Course Demo
Website: JustAcademy Official Website
1. Introduction to Mock-Based Testing
Mock-based testing is a unit-testing technique in which developers replace real dependencies with simulated objects called mocks. These mock objects allow a test to control dependency behavior, provide predefined responses, and verify how the class under test interacts with other components.
In Java applications, a service may depend on a repository, payment gateway, email service, or external API. Testing the service with all real dependencies can make tests slow, complicated, and dependent on databases or network availability. Mockito helps isolate the service so its business logic can be tested independently.
Mockito is a popular Java mocking framework that works with testing frameworks such as JUnit. It supports creating mocks, defining behavior, verifying method calls, and handling exceptions during unit tests.
2. Learning Objectives
- Understand unit testing, mock objects, and dependency isolation.
- Learn the purpose of Mockito in Java application testing.
- Create mock objects using
mock() and @Mock.
- Inject mock dependencies using
@InjectMocks.
- Configure mock behavior with
when() and thenReturn().
- Verify interactions using
verify().
- Test exceptions, argument matching, and consecutive method calls.
- Write maintainable Mockito tests using JUnit 5.
- Recognize common Mockito mistakes and apply testing best practices.
3. What Is Unit Testing?
Unit testing checks a small, isolated part of an application, such as a method or class, to confirm that it behaves as expected for given inputs.
Consider an order service that calculates an order total and retrieves product information from a repository. A unit test can focus on the service's logic without requiring a real database connection.
Benefits of Unit Testing
- Detects defects early in development.
- Provides confidence when refactoring code.
- Documents expected behavior through executable examples.
- Reduces reliance on external services during testing.
- Helps isolate the cause of failures.
- Supports continuous integration and automated quality checks.
4. What Is Mockito?
Mockito is a Java framework used to create test doubles, configure their behavior, and verify interactions with them. A test double is an object that stands in for a real dependency during testing.
Mockito is particularly useful when the class being tested depends on interfaces or collaborators that should not be executed in a unit test.
Example Scenario
Suppose an application contains an OrderService that calls an OrderRepository to save orders. The test does not need to write to a real database. A mocked repository can return a predefined order, allowing the test to focus on the service's behavior.
Advantages of Mockito
- Creates mock objects without implementing every interface manually.
- Allows predictable responses from dependencies.
- Verifies whether expected interactions occurred.
- Supports exception simulation and edge-case testing.
- Integrates with JUnit and other Java testing tools.
5. Understanding Mocks, Stubs, and Spies
| Concept |
Purpose |
Example |
| Mock |
Simulates a dependency and supports interaction verification. |
A mocked repository that returns a configured customer. |
| Stub |
Provides predefined responses to method calls. |
A dependency configured to return a fixed price. |
| Spy |
Wraps a real object so real behavior can run unless specific methods are stubbed. |
A real calculator wrapped with a spy to observe selected calls. |
| Fake |
Provides a simplified working implementation. |
An in-memory repository used instead of a database. |
Mockito's mocks and spies are related but serve different purposes. A regular mock does not execute the real implementation of ordinary methods unless configured otherwise. A spy wraps an actual object and can execute real methods, so it requires additional care.
6. Setting Up Mockito with JUnit 5
For a Maven project, add the JUnit Jupiter and Mockito dependencies. The versions shown below are examples; use compatible, current versions approved for your project.
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.11.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.14.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>5.14.2</version>
<scope>test</scope>
</dependency>
</dependencies>
These versions illustrate the dependency structure rather than prescribing the latest releases. Check compatibility with your Java version, build configuration, and existing dependency-management setup.
Why These Dependencies Are Needed
junit-jupiter provides the JUnit 5 testing API and test engine support.
mockito-core provides Mockito's core mocking and verification features.
mockito-junit-jupiter integrates Mockito with JUnit 5 through the Mockito extension.
7. Creating Your First Mock
The mock() method creates a mock of a class or interface.
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
class CalculatorService {
int add(int a, int b) {
return a + b;
}
}
class CalculatorServiceTest {
@Test
void shouldReturnConfiguredMockValue() {
CalculatorService calculator =
mock(CalculatorService.class);
when(calculator.add(2, 3)).thenReturn(10);
assertEquals(10, calculator.add(2, 3));
}
}
In this example, Mockito does not run the original add() implementation. The test configures the mock to return 10 for the specified invocation.
Testing principle: Mocking a class that contains the business logic being tested often defeats the purpose of a unit test. In real projects, usually test the actual service and mock its external collaborators.
8. Creating Mocks with @Mock
The @Mock annotation is a convenient alternative to calling mock() manually for each dependency.
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.mockito.Mockito.when;
import static org.junit.jupiter.api.Assertions.assertEquals;
@ExtendWith(MockitoExtension.class)
class CustomerRepositoryTest {
@Mock
private CustomerRepository customerRepository;
@Test
void shouldReturnConfiguredCustomerName() {
when(customerRepository.findNameById(1L))
.thenReturn("Aarav");
assertEquals(
"Aarav",
customerRepository.findNameById(1L)
);
}
}
The @ExtendWith(MockitoExtension.class) annotation initializes Mockito annotations and integrates Mockito's lifecycle with JUnit 5. Without a Mockito extension or another initialization mechanism, annotated mocks may remain uninitialized.
9. Using @InjectMocks to Create the Class Under Test
The @InjectMocks annotation asks Mockito to create the object under test and inject available mocks or spies into it. Constructor injection is generally the clearest dependency-injection pattern for application services.
import java.util.Optional;
interface ProductRepository {
Optional<Product> findById(Long id);
}
class Product {
private final Long id;
private final String name;
Product(Long id, String name) {
this.id = id;
this.name = name;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
}
class ProductService {
private final ProductRepository repository;
ProductService(ProductRepository repository) {
this.repository = repository;
}
public String getProductName(Long id) {
return repository.findById(id)
.map(Product::getName)
.orElse("Product not found");
}
}
import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.mockito.Mockito.when;
import static org.junit.jupiter.api.Assertions.assertEquals;
@ExtendWith(MockitoExtension.class)
class ProductServiceTest {
@Mock
private ProductRepository repository;
@InjectMocks
private ProductService productService;
@Test
void shouldReturnProductName() {
Product product = new Product(1L, "Keyboard");
when(repository.findById(1L))
.thenReturn(Optional.of(product));
assertEquals(
"Keyboard",
productService.getProductName(1L)
);
}
@Test
void shouldReturnNotFoundWhenProductIsMissing() {
when(repository.findById(99L))
.thenReturn(Optional.empty());
assertEquals(
"Product not found",
productService.getProductName(99L)
);
}
}
This example tests the real ProductService while replacing its repository dependency with a mock. It covers both the successful lookup and the missing-product case.
10. Stubbing with when() and thenReturn()
Stubbing defines how a mock should behave when a particular method is called with specific arguments. Mockito provides when() and thenReturn() for configuring return values.
when(repository.findById(1L))
.thenReturn(Optional.of(new Product(1L, "Mouse")));
when(repository.findById(2L))
.thenReturn(Optional.empty());
The first invocation returns a product, while the second returns an empty optional.
Returning Different Values on Consecutive Calls
when(repository.getStatus())
.thenReturn("PENDING")
.thenReturn("PROCESSING")
.thenReturn("COMPLETED");
Consecutive calls return the configured values in order. Subsequent calls continue returning the last configured value.
11. Using Argument Matchers
Argument matchers let you configure behavior for a range of inputs instead of one exact value. Common matchers include any(), anyString(), anyLong(), and eq().
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.when;
when(customerRepository.findName(anyString()))
.thenReturn("Test Customer");
Any non-null string argument matched by anyString() receives the configured response. If null is a valid argument, use an appropriate matcher such as nullable(String.class) or another matcher that explicitly supports null.
Combining Matchers
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
when(priceService.calculatePrice(
anyLong(), eq("INR")))
.thenReturn(1500.0);
When using argument matchers for a method call, use matchers consistently for all arguments that need matching. For example, do not combine a matcher with an ordinary literal argument in the same invocation; use eq() for the literal value.
12. Verifying Interactions with verify()
Mockito's verify() method checks whether a mocked dependency was called as expected. It is useful when a service should invoke a repository, notification service, or another collaborator.
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.junit.jupiter.api.Assertions.assertEquals;
@Test
void shouldFindProductAndVerifyRepositoryCall() {
Product product = new Product(1L, "Monitor");
when(repository.findById(1L))
.thenReturn(Optional.of(product));
String name = productService.getProductName(1L);
assertEquals("Monitor", name);
verify(repository).findById(1L);
}
This test checks both the returned result and the repository interaction. Verification is most useful when the interaction itself matters to the behavior being tested.
Verifying Invocation Counts
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
verify(repository, times(1)).findById(1L);
verify(repository, atLeastOnce()).findById(1L);
verify(repository, never()).findById(99L);
| Method |
Meaning |
verify(mock) |
Verifies one invocation by default. |
times(n) |
Verifies an exact number of invocations. |
atLeastOnce() |
Verifies that the method was called one or more times. |
atLeast(n) |
Verifies a minimum number of invocations. |
atMost(n) |
Verifies a maximum number of invocations. |
never() |
Verifies that the method was not called. |
13. Testing Methods That Throw Exceptions
Applications should respond appropriately when repositories, external services, or other dependencies fail. Mockito can simulate exceptions so the test can verify the service's error-handling behavior.
import static org.mockito.Mockito.when;
import static org.junit.jupiter.api.Assertions.assertThrows;
@Test
void shouldPropagateRepositoryFailure() {
when(repository.findById(1L))
.thenThrow(new RuntimeException("Database unavailable"));
assertThrows(
RuntimeException.class,
() -> productService.getProductName(1L)
);
}
This example assumes the service allows the repository exception to propagate. If the service catches the exception and converts it into a custom exception or fallback response, test that expected behavior instead.
Testing Void Methods
Void methods do not return values. Use doThrow() when a void method needs to throw an exception during a test.
import static org.mockito.Mockito.doThrow;
doThrow(new RuntimeException("Email service unavailable"))
.when(emailService)
.sendEmail("[email protected]");
You can then invoke the class under test and verify whether it handles or propagates the simulated failure correctly.
14. Using doReturn(), doThrow(), and doNothing()
Mockito provides the doReturn(), doThrow(), and doNothing() family of methods for particular stubbing scenarios.
// Configure a return value.
doReturn("Active")
.when(statusService)
.getStatus();
// Configure an exception for a void method.
doThrow(new IllegalStateException("Operation failed"))
.when(notificationService)
.sendNotification();
// Void methods on regular mocks do nothing by default.
doNothing()
.when(notificationService)
.sendNotification();
doNothing() is usually unnecessary for an ordinary mocked void method because that is its default behavior. The doReturn() and doThrow() forms can also be useful with spies, where the ordinary when(spy.method()) syntax may execute the real method during stubbing.
15. Using @Spy
A spy wraps a real object and allows selected behavior to be stubbed or interactions to be verified. Unlike a regular mock, a spy generally calls real methods when they have not been stubbed.
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.doReturn;
import static org.junit.jupiter.api.Assertions.assertEquals;
class DiscountCalculator {
double calculate(double price) {
return price * 0.90;
}
}
@Test
void shouldUseConfiguredSpyBehavior() {
DiscountCalculator calculator =
spy(new DiscountCalculator());
doReturn(50.0)
.when(calculator)
.calculate(100.0);
assertEquals(50.0, calculator.calculate(100.0));
assertEquals(90.0, calculator.calculate(100.0 - 0.0 + 100.0));
}
The final assertion above is intentionally not a valid expected result for the configured spy: the method call is still calculate(100.0), so the configured result is 50.0. A correct second assertion would test a different, unstubbed input, such as calculate(200.0), which should return 180.0. This illustrates why expected values should be checked carefully when testing spies.
Prefer ordinary mocks for external dependencies and use spies only when partial real behavior is genuinely needed. If a class is difficult to test without a spy, consider simplifying its responsibilities.
16. Capturing Arguments with ArgumentCaptor
ArgumentCaptor captures an argument passed to a mock so the test can inspect its contents. It is useful when a service builds an object and passes it to a repository or notification component.
import org.mockito.ArgumentCaptor;
import static org.mockito.Mockito.verify;
import static org.junit.jupiter.api.Assertions.assertEquals;
@Test
void shouldCaptureSavedProduct() {
Product product = new Product(1L, "Keyboard");
repository.save(product);
ArgumentCaptor<Product> captor =
ArgumentCaptor.forClass(Product.class);
verify(repository).save(captor.capture());
assertEquals("Keyboard", captor.getValue().getName());
}
This example assumes the repository mock has a save(Product) method and the test invokes the code responsible for saving the product. In a service test, call that service method first, then capture the argument passed to the mocked repository.
17. Verifying the Order of Interactions
Sometimes a workflow requires dependencies to be called in a particular sequence. Mockito's InOrder API can verify that sequence.
import org.mockito.InOrder;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.verify;
InOrder order = inOrder(paymentService, notificationService);
verify(paymentService).charge();
verify(notificationService).sendReceipt();
Use ordered verification only when call order is an important part of the business requirement. Verifying every interaction in every test can make tests unnecessarily fragile.
18. Testing Service Logic with Multiple Dependencies
A service may depend on several collaborators. Mockito allows each dependency to be mocked independently so the test can control each response.
interface InventoryService {
boolean isAvailable(Long productId);
}
interface PaymentService {
boolean charge(double amount);
}
class CheckoutService {
private final InventoryService inventoryService;
private final PaymentService paymentService;
CheckoutService(
InventoryService inventoryService,
PaymentService paymentService) {
this.inventoryService = inventoryService;
this.paymentService = paymentService;
}
public String checkout(Long productId, double amount) {
if (!inventoryService.isAvailable(productId)) {
return "OUT_OF_STOCK";
}
if (!paymentService.charge(amount)) {
return "PAYMENT_FAILED";
}
return "SUCCESS";
}
}
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.never;
import static org.junit.jupiter.api.Assertions.assertEquals;
@ExtendWith(MockitoExtension.class)
class CheckoutServiceTest {
@Mock
private InventoryService inventoryService;
@Mock
private PaymentService paymentService;
@InjectMocks
private CheckoutService checkoutService;
@Test
void shouldCompleteCheckoutWhenStockAndPaymentAreAvailable() {
when(inventoryService.isAvailable(10L))
.thenReturn(true);
when(paymentService.charge(500.0))
.thenReturn(true);
assertEquals(
"SUCCESS",
checkoutService.checkout(10L, 500.0)
);
verify(inventoryService).isAvailable(10L);
verify(paymentService).charge(500.0);
}
@Test
void shouldNotChargeWhenProductIsOutOfStock() {
when(inventoryService.isAvailable(10L))
.thenReturn(false);
assertEquals(
"OUT_OF_STOCK",
checkoutService.checkout(10L, 500.0)
);
verify(paymentService, never()).charge(500.0);
}
}
The tests focus on the actual checkout logic and control the inventory and payment dependencies. They verify both the returned status and an important interaction rule: payment must not be attempted when stock is unavailable.
19. Mockito in Spring Boot Applications
Mockito can test ordinary Java service classes without starting a Spring application context. This is usually the fastest approach for unit tests.
For Spring Boot applications, distinguish between isolated unit tests and integration tests:
- Unit tests: Instantiate the service and mock its dependencies using Mockito.
- Spring integration tests: Start the relevant Spring context to verify configuration, dependency injection, repository integration, or application wiring.
- Web-layer tests: Test controllers using suitable Spring test tools and mock the service layer where appropriate.
Spring Boot's test annotations and mock-bean APIs vary by version. Use the approach supported by the Spring Boot version in the project rather than mixing incompatible testing APIs.
20. Common Mockito Mistakes
| Mistake |
Why It Happens |
Solution |
| Mocks are null |
Mockito annotations were not initialized. |
Use @ExtendWith(MockitoExtension.class) or an appropriate initialization mechanism. |
| Real service logic never executes |
The class under test was mocked instead of instantiated. |
Test a real service instance and mock its collaborators. |
| Stubbed value is not returned |
The arguments differ from the configured invocation. |
Check argument values and use suitable matchers. |
| Invalid matcher usage |
Matchers and raw values are mixed in the same call. |
Use matchers consistently, such as eq() for literal arguments. |
| Spy executes unexpected real code |
Stubbing with when() invokes a real method on the spy. |
Use the doReturn() or doThrow() style when appropriate. |
| Test passes but business logic is untested |
The test only verifies interactions or mocked outputs. |
Assert meaningful results and test the actual class under test. |
| Too many brittle tests |
Every internal interaction is verified unnecessarily. |
Verify only interactions that matter to observable behavior. |
| Unnecessary stubbing warnings |
The test configures behavior that is never used. |
Remove unused stubs and keep setup focused on each test. |
21. Best Practices for Mock-Based Testing
- Follow the Arrange–Act–Assert pattern: prepare the test, execute the behavior, and assert the outcome.
- Use descriptive test names that explain the expected behavior.
- Mock external collaborators rather than the business logic under test.
- Prefer constructor injection in production classes because it makes dependencies explicit.
- Configure only the stubbing needed by each test.
- Verify interactions only when they are relevant to the requirement.
- Cover success, missing data, invalid inputs, and dependency failures.
- Avoid relying on network calls, real databases, or current time in isolated unit tests.
- Use real value objects and simple data models instead of mocking everything.
- Do not use reflection or static mocking as a first resort when dependency injection can improve testability.
- Keep unit tests fast, deterministic, and independent of execution order.
- Use integration tests in addition to unit tests for behavior that depends on real infrastructure.
22. Practical Project: Unit Testing an Order Service
Project Objective
Create a small order-processing service and use Mockito to test order lookup, successful processing, missing orders, payment failures, and notification behavior.
Requirements
- Create an order model containing an order ID, customer ID, and amount.
- Create an order repository interface.
- Create a payment service interface.
- Create a notification service interface.
- Implement an order service that coordinates these dependencies.
- Mock the repository, payment service, and notification service.
- Stub each dependency to simulate success and failure conditions.
- Assert the service's result for each scenario.
- Verify that notifications are sent only when the relevant business operation succeeds.
- Run the tests using Maven or Gradle in the development environment or continuous integration pipeline.
Suggested Project Structure
src
├── main
│ └── java
│ └── com.example.orders
│ ├── model
│ │ └── Order.java
│ ├── repository
│ │ └── OrderRepository.java
│ ├── service
│ │ ├── OrderService.java
│ │ ├── PaymentService.java
│ │ └── NotificationService.java
└── test
└── java
└── com.example.orders
└── service
└── OrderServiceTest.java
Expected Learning Outcomes
- Build isolated unit tests for Java service classes.
- Control dependency behavior with Mockito stubbing.
- Verify meaningful interactions between service components.
- Test error paths without needing real payment or notification systems.
- Improve confidence in code changes and refactoring.
23. Interview Questions and Answers
Q1. What is Mockito?
Answer: Mockito is a Java mocking framework that creates test doubles, configures their behavior, and verifies interactions during tests.
Q2. What is a mock object?
Answer: A mock is a simulated dependency used to isolate the class under test and control how its collaborators behave.
Q3. What is the difference between @Mock and @InjectMocks?
Answer: @Mock creates a mock dependency. @InjectMocks asks Mockito to create the object under test and inject available mock or spy dependencies.
Q4. What is stubbing?
Answer: Stubbing defines a mock's response for a particular method invocation, often using when(...).thenReturn(...).
Q5. What is the purpose of verify()?
Answer: It checks whether a mocked method was called with the expected arguments and invocation count.
Q6. What is the difference between a mock and a spy?
Answer: A mock simulates dependency behavior. A spy wraps a real object and generally calls real methods unless their behavior is stubbed.
Q7. How do you mock an exception?
Answer: Use when(...).thenThrow(...) for suitable methods or doThrow(...).when(...) for void methods and other applicable scenarios.
Q8. What is ArgumentCaptor?
Answer: It captures an argument passed to a mock so the test can inspect its properties after the interaction occurs.
Q9. Why use MockitoExtension in JUnit 5?
Answer: It initializes Mockito annotations and integrates Mockito's test lifecycle and validation with JUnit Jupiter.
Q10. What are argument matchers?
Answer: Matchers such as anyString(), anyLong(), and eq() allow a test to match method arguments flexibly.
Q11. Should every method call be verified?
Answer: No. Verify interactions that are meaningful to the behavior under test. Excessive verification can make tests brittle.
Q12. Can Mockito replace all integration testing?
Answer: No. Mockito isolates dependencies, but integration tests are still needed to verify real database interactions, application wiring, and communication with infrastructure.
Q13. What causes an unnecessary stubbing warning?
Answer: A mock was configured for a method call that did not occur during the test. Remove the unused stubbing or revise the test setup.
Q14. How do you test a service that depends on a repository?
Answer: Instantiate the real service, mock the repository, configure the repository's responses, invoke the service method, and assert the expected output or exception.
Q15. What makes a good unit test?
Answer: A good unit test is focused, deterministic, readable, independent, and verifies meaningful behavior rather than implementation details.
24. Practice Exercises
- Create a mock repository and configure it to return an existing user.
- Test a service method that returns a fallback message when a record is missing.
- Use
verify() to confirm that a repository method was called exactly once.
- Verify that a notification service is never called when payment fails.
- Use argument matchers to configure a dependency for multiple possible inputs.
- Use
ArgumentCaptor to inspect an object passed to a mocked repository.
- Simulate an exception from an external API and test the service's response.
- Write a test that verifies the order of two important collaborator calls.
- Compare a mock and a spy using a small Java class.
- Run the tests automatically during a Maven build.
25. Quick Revision Checklist
- Mockito creates mocks and supports stubbing and verification.
- JUnit 5 can integrate with Mockito through
MockitoExtension.
@Mock creates a mock dependency.
@InjectMocks creates the class under test and attempts dependency injection.
when() and thenReturn() configure mock responses.
thenThrow() and doThrow() simulate exceptions.
verify() checks method interactions.
- Argument matchers support flexible argument matching.
ArgumentCaptor inspects arguments passed to a mock.
- Spies can execute real methods and require careful stubbing.
- Good unit tests focus on real business logic and mock external collaborators.
- Integration tests remain important for verifying actual infrastructure behavior.
26. Conclusion
Mockito makes Java unit testing more effective by allowing developers to isolate a class from its dependencies, simulate different outcomes, and verify important interactions. When combined with JUnit 5, it supports clear and maintainable tests for success scenarios, missing data, exceptions, and complex service workflows.
The most important principle is to test the real behavior of the class under test while mocking only the dependencies necessary for isolation. Use focused stubbing, meaningful assertions, selective verification, and integration tests where real infrastructure must be validated.
27. Additional Learning Resources